Since 2015, JavaScript has improved immensely.
It’s much more pleasant to use it now than ever.
In this article, we’ll look at metaprogramming with JavaScript proxies.
Wrapping Instances of Built-in Constructors
We can use proxies to wrap instances of built-in constructors.
For instance, we can write:
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
But if we write:
console.log(proxy.getDate());
We get ‘Uncaught TypeError: this is not a Date object.’ because we called getDate
on the proxy instead of the original Date
instance.
However, proxies can be wrapped transparently with arrays.
For instance, we can write:
const p = new Proxy(new Array(), {});
p.push('foo');
console.log(p.length);
We created a proxy with the Array
constructor.
The handler object is an empty object.
We call push
with to insert an entry into the array.
It works with the proxy.
And we get the length
with as we expected.
For objects that can’t be transparently wrapped with proxies, we can modify the handler so that it does what we expect.
For instance, we can write:
const handler = {
get(target, propKey, receiver) {
if (typeof target[propKey] === 'function') {
return target[propKey].bind(target);
}
return target[propKey]
},
};
const target = new Date();
const proxy = new Proxy(target, handler);
Then we call the target
‘s propKey
method.
In the get
method, we check if target[propKey]
‘s type is a 'function'
.
If it is, then we change the value of this
with bind
so that the function will have the correct value of this
.
Otherwise, we just return the value as-is.
Now we can call the getDate
method on the object without issue:
const handler = {
get(target, propKey, receiver) {
if (typeof target[propKey] === 'function') {
return target[propKey].bind(target);
}
return target[propKey]
},
};
const target = new Date();
const proxy = new Proxy(target, handler);
console.log(proxy.getDate());
Use Cases for Proxies
We can use proxies to log the operations of the object.
For example, we can write:
const handler = {
get(target, propKey, receiver) {
console.log(target[propKey])
if (typeof target[propKey] === 'function') {
return target[propKey].bind(target);
}
return target[propKey]
},
};
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
toString() {
return `Person(${this.firstName}, ${this.lastName})`;
}
}
const target = new Person('jane', 'smith');
const proxy = new Proxy(target, handler);
console.log(proxy.toString());
to trace the object operation.
We get the structure of the toString
method.
And then we get the returned result with it.
We can also use proxies to warn about unknown properties.
For instance, we can write:
const target = {};
const handler = {
get(target, propKey, receiver) {
if (!(propKey in target)) {
throw new Error('property does not exist');
}
return target[propKey];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo);
We have a handler
object that has a get
method.
Inside it, we use the in
operator to check if there’s a property with the name propKey
.
Since there’s no foo
property in target
, we get ‘Uncaught Error: property does not exist’.
This is a handy feature that doesn’t in JavaScript objects natively.
Conclusion
We can use proxies to log object operations and intercept object operations in various ways.